#include "twitch.h"
#include "painter.h"
#include "base/file.h"
#include "base/json.h"
#include "app.h"

static Image* loadIcon(char const* path, uint32 color)
{
  Image* img = new Image(18, 18);
  img->fill(color);
  File* file = File::openURL(path);
  if (file)
  {
    Image src(file);
    img->blt((img->width() - src.width()) / 2,
             (img->height() - src.height()) / 2, &src);
    delete file;
  }
  return img;
}
static Image* loadIcon(char const* path)
{
  Image* img = NULL;
  File* file = File::openURL(path);
  if (file)
  {
    img = new Image(file);
    if (img->bits() == NULL)
    {
      delete img;
      img = NULL;
    }
    delete file;
  }
  return img;
}
Image* EmoticonImage::getImage()
{
  if (image)
    return image;
  image = loadIcon(url);
  return image;
}
EmoticonImage* Emoticon::getImage(Array<int> const* emoteset)
{
  EmoticonImage* best = NULL;
  for (int i = 0; i < images.length(); i++)
  {
    EmoticonImage* cur = &images[i];
    if (cur->emoteset == 717)
      int asdf = 0;
    if (cur->emoteset != 0)
    {
      bool found = false;
      if (emoteset)
        for (int j = 0; j < emoteset->length() && !found; j++)
          if ((*emoteset)[j] == cur->emoteset)
            found = true;
      if (!found)
        cur = NULL;
    }
    if (cur && (best == NULL || best->emoteset < cur->emoteset))
      best = cur;
  }
  return best;
}

TwitchClient::TwitchClient()
  : ircLoopThread(NULL)
  , users(DictionaryMap::alNumNoCase)
  , mainWnd(NULL)
  , emoteLoadThread(NULL)
  , infoThread(NULL)
  , emoticons(NULL)
  , stopLoading(false)
{
  info.viewers = 0;
  memset(specialIcons, 0, sizeof specialIcons);

  irc.hook_irc_command("PRIVMSG", onPrivMsg, this);
  irc.hook_irc_command("MODE", onMode, this);
  irc.hook_irc_command("NOTICE", onNotice, this);
  irc.hook_irc_command("001", on001, this);

  InitializeCriticalSection(&infoLock);

  emoteLoadThread = CreateThread(NULL, 0, emoteLoadFunc, this, 0, NULL);
  infoThread = CreateThread(NULL, 0, infoFunc, this, 0, NULL);
}
TwitchClient::~TwitchClient()
{
  stopLoading = true;
  disconnect();
  if (emoteLoadThread)
  {
    WaitForSingleObject(emoteLoadThread, INFINITE);
    CloseHandle(emoteLoadThread);
  }
  if (infoThread)
  {
    WaitForSingleObject(infoThread, INFINITE);
    CloseHandle(infoThread);
  }
  delete emoticons;
  DeleteCriticalSection(&infoLock);
  for (uint32 cur = images.enumStart(); cur; cur = images.enumNext(cur))
    delete images.enumGetValue(cur);
}
DWORD WINAPI TwitchClient::emoteLoadFunc(LPVOID arg)
{
  TwitchClient* tc = (TwitchClient*) arg;
  File* emoticonsFile = File::openURL("https://api.twitch.tv/kraken/chat/emoticons");
  json::Value* emoticonsValue = NULL;
  if (emoticonsFile)
  {
    if (!tc->stopLoading)
      emoticonsValue = json::Value::parse(emoticonsFile);
    delete emoticonsFile;
  }
  if (emoticonsValue && !tc->stopLoading)
  {
    json::Value* list = emoticonsValue->get("emoticons");
    if (list && list->type() == json::Value::tArray)
    {
      Array<Emoticon>* emoticons = new Array<Emoticon>;
      for (uint32 i = 0; i < list->length() && !tc->stopLoading; i++)
      {
        if (i == 54)
          int asdf = 0;
        json::Value* emoticon = list->at(i);
        json::Value* regex = emoticon->get("regex");
        json::Value* images = emoticon->get("images");
        if (regex && regex->type() == json::Value::tString &&
          images && images->type() == json::Value::tArray)
        {
          Emoticon& e = emoticons->push();
          e.prog = new pcrecpp::RE(regex->getString());
          for (uint32 j = 0; j < images->length(); j++)
          {
            json::Value* image = images->at(j);
            json::Value* width = image->get("width");
            json::Value* height = image->get("height");
            json::Value* url = image->get("url");
            json::Value* emoticon_set = image->get("emoticon_set");
            if (width && width->type() == json::Value::tNumber &&
              height && height->type() == json::Value::tNumber &&
              url && url->type() == json::Value::tString &&
              emoticon_set && (emoticon_set->type() == json::Value::tNumber ||
                               emoticon_set->type() == json::Value::tNull))
            {
              EmoticonImage& img = e.images.push();
              img.width = width->getInteger();
              img.height = height->getInteger();
              img.url = url->getString();
              img.emoteset = (emoticon_set->type() == json::Value::tNumber ? emoticon_set->getInteger() : 0);
            }
          }
        }
      }
      tc->emoticons = emoticons;
    }
    delete emoticonsValue;
  }
  return 0;
}

void TwitchClient::connect(char const* name, char const* pass, char const* channel, uint32 port)
{
  disconnect();

  specialIcons[UserInfo::sMod] = loadIcon("http://www-cdn.jtvnw.net/images/xarth/g/g18_sword-FFFFFF80.png", 0xFF009900);

  File* badgesFile = File::openURL(String::format("https://api.twitch.tv/kraken/chat/%s/badges", channel + 1));
  json::Value* badges = NULL;
  if (badgesFile)
  {
    badges = json::Value::parse(badgesFile);
    delete badgesFile;
  }
  if (badges)
  {
    for (uint32 cur = badges->enumStart(); cur; cur = badges->enumNext(cur))
    {
      String key = badges->enumGetKey(cur);
      json::Value* value = badges->enumGetValue(cur);
      if (value->type() == json::Value::tObject)
      {
        json::Value* image = value->get("image");
        if (image && image->type() == json::Value::tString)
        {
          if (key == "admin")
            specialIcons[UserInfo::sAdmin] = loadIcon(image->getString());
          else if (key == "broadcaster")
            specialIcons[UserInfo::sBroadcaster] = loadIcon(image->getString());
          else if (key == "staff")
            specialIcons[UserInfo::sStaff] = loadIcon(image->getString());
          else if (key == "turbo")
            specialIcons[UserInfo::sTurbo] = loadIcon(image->getString());
          else if (key == "subscriber")
            specialIcons[UserInfo::sSubscriber] = loadIcon(image->getString());
        }
      }
    }
  }

  userName = name;
  EnterCriticalSection(&infoLock);
  channelName = channel;
  LeaveCriticalSection(&infoLock);
  irc.start("irc.twitch.tv", port, name, pass);
  irc.write("TWITCHCLIENT\r\n");
  irc.join(channel);
  ircLoopThread = CreateThread(NULL, 0, ircLoopFunc, &irc, 0, NULL);
}
DWORD WINAPI TwitchClient::infoFunc(LPVOID arg)
{
  TwitchClient* tc = (TwitchClient*) arg;
  
  while (!tc->stopLoading)
  {
    EnterCriticalSection(&tc->infoLock);
    String channelName = tc->channelName;
    LeaveCriticalSection(&tc->infoLock);

    ChannelInfo info;
    info.logo = NULL;
    if (channelName.length())
    {
      info.title = String::format("(Offline) %s", channelName.c_str() + 1);
      info.viewers = 0;
      File* ifile = File::openURL(String::format("https://api.twitch.tv/kraken/streams/%s",
        channelName.c_str() + 1));
      json::Value* root = NULL;
      if (ifile)
      {
        root = json::Value::parse(ifile);
        delete ifile;
      }
      json::Value* channel = NULL;
      if (root)
      {
        json::Value* cur = root->get("stream");
        if (cur && cur->type() == json::Value::tObject)
        {
          json::Value* viewers = cur->get("viewers");
          if (viewers && viewers->type() == json::Value::tNumber)
            info.viewers = viewers->getInteger();
          cur = cur->get("channel");
          if (cur && cur->type() == json::Value::tObject)
            channel = cur;
        }
      }
      if (channel == NULL)
      {
        delete root;
        ifile = File::openURL(String::format("https://api.twitch.tv/kraken/channels/%s",
          channelName.c_str() + 1));
        if (ifile)
        {
          root = json::Value::parse(ifile);
          channel = root;
          delete ifile;
        }
      }
      if (channel)
      {
        json::Value* status = channel->get("status");
        json::Value* logo = channel->get("logo");
        if (status && status->type() == json::Value::tString)
          info.title = status->getString();
        if (logo && logo->type() == json::Value::tString)
          info.logourl = logo->getString();
      }
      else
        info.title = channelName.substring(1);
      if (info.viewers == 0)
        info.title = "(Offline) " + info.title;
      delete root;
    }
    else
      info.viewers = 0;

    EnterCriticalSection(&tc->infoLock);
    tc->info = info;
    LeaveCriticalSection(&tc->infoLock);

    Sleep(500);
  }

  return 0;
}
Image* TwitchClient::getImage(char const* url)
{
  if (images.has(url))
    return images.get(url);
  Image* img = loadIcon(url);
  images.set(url, img);
  return img;
}
ChannelInfo* TwitchClient::getInfo()
{
  ChannelInfo* res = NULL;
  EnterCriticalSection(&infoLock);
  if (info.title.length())
    res = new ChannelInfo(info);
  LeaveCriticalSection(&infoLock);
  if (res && res->logourl.length())
    res->logo = getImage(res->logourl);
  return res;
}

void TwitchClient::disconnect()
{
  EnterCriticalSection(&infoLock);
  channelName = "";
  info.title = "";
  info.viewers = 0;
  LeaveCriticalSection(&infoLock);
  for (int i = 0; i < sizeof specialIcons / sizeof specialIcons[0]; i++)
    delete specialIcons[i];
  memset(specialIcons, 0, sizeof specialIcons);
  irc.disconnect();
  if (ircLoopThread)
  {
    WaitForSingleObject(ircLoopThread, INFINITE);
    CloseHandle(ircLoopThread);
    ircLoopThread = NULL;
  }
}

DWORD WINAPI TwitchClient::ircLoopFunc(LPVOID arg)
{
  ((IRC*) arg)->message_loop();
  PostMessage(getApp()->getMainWindow(), TCM_DISCONNECTED, 0, 0);
  return 0;
}
int TwitchClient::onMode(char const* msg, irc_reply_data* data, IRC* irc, void* opaque)
{
  TwitchClient* tc = (TwitchClient*) opaque;

  UserInfo* user = tc->users.getptr(msg);
  if (user == NULL)
  {
    user = &tc->users.create(msg);
    user->color = 0xFF0000;
    user->special = 0;
  }

  if (!stricmp(tc->channelName.c_str() + 1, msg))
    user->special |= UserInfo::fBroadcaster;
  else
    user->special |= UserInfo::fMod;
  return 0;
}
int TwitchClient::onNotice(char const* msg, irc_reply_data* data, IRC* irc, void* opaque)
{
  TwitchClient* tc = (TwitchClient*) opaque;
  if (tc->mainWnd)
    tc->mainWnd->addMessage(CHAT_SYSTEM, NULL, msg + 1);
  return 0;
}
int TwitchClient::on001(char const* msg, irc_reply_data* data, IRC* irc, void* opaque)
{
  TwitchClient* tc = (TwitchClient*) opaque;
  if (tc->mainWnd)
    tc->mainWnd->addMessage(CHAT_CONNECTED, NULL, "Welcome to the chat room!");
  return 0;
}
int TwitchClient::onPrivMsg(char const* msg, irc_reply_data* data, IRC* irc, void* opaque)
{
  TwitchClient* tc = (TwitchClient*) opaque;
  if (tc->mainWnd && !stricmp(data->target, tc->channelName))
    tc->mainWnd->addMessage(CHAT_MESSAGE, data->nick, msg + 1);
  else if (!stricmp(data->target, tc->userName))
  {
    Array<String> args;
    String(msg + 1).split(args, ' ');
    if (args.length() == 3 && (args[0].icompare("specialuser") == 0 ||
                               args[0].icompare("usercolor") == 0 ||
                               args[0].icompare("emoteset") == 0))
    {
      UserInfo* user = tc->users.getptr(args[1]);
      if (user == NULL)
      {
        user = &tc->users.create(args[1]);
        user->color = 0xFF0000;
        user->special = 0;
      }

      if (args[0].icompare("specialuser") == 0)
      {
        if (args[2].icompare("subscriber") == 0)
          user->special |= UserInfo::fSubscriber;
        if (args[2].icompare("turbo") == 0)
          user->special |= UserInfo::fTurbo;
        if (args[2].icompare("admin") == 0)
          user->special |= UserInfo::fAdmin;
        if (args[2].icompare("staff") == 0)
          user->special |= UserInfo::fStaff;
      }
      else if (args[0].icompare("usercolor") == 0)
      {
        if (args[2][0] == '#' && args[2].length() == 7)
          sscanf(args[2].c_str() + 1, "%x", &user->color);
      }
      else if (args[0].icompare("emoteset") == 0)
      {
        if (args[2][0] == '[' && args[2][args[2].length() - 1] == ']')
        {
          user->emoteset.clear();
          Array<String> emoteset;
          args[2].substring(1, -1).split(emoteset, ',');
          for (int i = 0; i < emoteset.length(); i++)
            user->emoteset.push(emoteset[i].toInt());
        }
      }
    }
    else if (args.length() && args[0].icompare("clearchat") == 0)
    {
      if (args.length() > 1)
        tc->mainWnd->addMessage(CHAT_CLEARUSER, args[1], "<message deleted>");
      else
      {
        tc->mainWnd->addMessage(CHAT_CLEAR, NULL, NULL);
        tc->mainWnd->addMessage(CHAT_SYSTEM, NULL, "Chat was cleared by a moderator.");
      }
    }
    else if (args.length() && args[0].icompare("historyend"))
      tc->mainWnd->addMessage(CHAT_SYSTEM, NULL, msg + 1);
  }
  return 0;
}

UserInfo const* TwitchClient::getUserInfo(char const* name) const
{
  static uint32 defColors[] = {
    0xFF0000, 0x0000FF, 0x00FF00, 0xB22222, 0xFF7F50,
    0x9ACD32, 0xFF4500, 0x2E8B57, 0xDAA520, 0xD2691E,
    0x5F9EA0, 0x1E90FF, 0xFF69B4, 0x8A2BE2, 0x00FF7F,
  };
  UserInfo const* ptr = users.getptr(name);
  if (ptr) return ptr;
  defaultUser.color = (name[0] ? defColors[(s_tolower(name[0]) +
    s_tolower(name[strlen(name) - 1])) % 15] : 0x000000);
  defaultUser.special = 0;
  return &defaultUser;
}

uint32 ChannelInfo::hash(ChannelInfo* info)
{
  if (info == NULL) return 0;
  uint32 hash = update_crc(0xFFFFFFFF, &info->viewers, sizeof info->viewers);
  hash = update_crc(hash, info->title.c_str(), info->title.length());
  return ~update_crc(hash, info->logourl.c_str(), info->logourl.length());
}

void TwitchClient::send(char const* msg)
{
  irc.privmsg(channelName, msg);
  irc_reply_data data;
  data.nick = userName.getBuffer();
  data.target = channelName.getBuffer();
  String msgb = String::format(":%s", msg);
  onPrivMsg(msgb.getBuffer(), &data, &irc, this);
}
